/**
 * @type {import('postcss').PluginCreator}
 */

const path = require('path')
const fs = require("fs").promises;
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const generator = require("@babel/generator").default;
const merge = require("deepmerge");
const utils = require("./lib/utils");
let classNames = require("./lib");

module.exports = (opts = {}) => {
  // Selectors that need to be converted
  const selectors = {};
  // Record the converted empty rule and delete it later
  const emptyRules = {};
  // Record @keyframes atRules
  const keyframesAtRules = {}
  // Record not converted decls
  const unTransformDecls = new Set()

  return {
    postcssPlugin: "postcss-transform-tailwindcss",
    prepare({ root }) {
      if (!root) {
        return
      }
      const file = root.source.input.file;
      const [filePath, extension] = utils.accessFileSyncfromCache(root.source.input.file, opts.fileExtensions)
      if (!filePath) {
        console.warn(`No translation file found for ${file}`)
        return
      }
      if (!file) {
        throw root.error(`
        Process options.from needs to be configured: postcss().process(css, {from: 'xxx.css'})`)
      }

      if (opts.getClassNames) {
        classNames = opts.getClassNames(classNames, merge);
      }

      root.walkAtRules(atRule => {
        if (atRule.name === 'keyframes') {
          const atRuleParams = atRule.params
          keyframesAtRules[atRuleParams] = utils.generateCSSRule('keyframes', atRuleParams, atRule.nodes)
        }
      })
      return {
        RootExit(root) {
          unTransformDecls.forEach((v) => {
            console.warn(`${v} has not been converted to tailwindcss at ${root.source.input.file} at ${filePath}.
            Please customize the tailwindcss class name and merge classNames through opts.getClassNames \n`)
          })
          Object.keys(selectors).forEach((key) => {
            if (key.split(' ').slice(0, -1).find((i) => (utils.isPseudoRule(i)) || i.slice(1).includes('.'))) {
              console.warn(`${key} has not been converted to tailwindcss at ${root.source.input.file} at ${filePath}.\n`)
            }
          })
          fs.readFile(filePath, { encoding: 'utf8' })
            .then((code) => {
              const ast = parser.parse(code, {
                sourceType: "module",
                plugins: ["jsx", "typescript"],
              });
              const targetAttribute = (extension === 'jsx' || extension === 'tsx') ? "className" : "class"
              traverse(ast, {
                JSXAttribute(path) {
                  const { node } = path;
                  const { name, value } = node
                  if (name && name.name === targetAttribute) {
                    const type = value && value.type
                    if (type === "StringLiteral") {
                      if (value.value) {
                        const classNames = value.value.split(" ");
                        const newValue = utils.getClassNames(path, classNames, selectors, targetAttribute);
                        value.value = newValue;
                      }
                    }
                    if (type === 'JSXExpressionContainer') {
                      const expression = value.expression
                      const { expressions, quasis, consequent, alternate } = expression
                      const identifiers = expressions?.filter((i) => i.type === 'Identifier' || (i.type === 'MemberExpression' && i.property.type === 'Identifier')) || []
                      const quasisClassNames = []

                      if (expression.type === 'TemplateLiteral') {
                        quasis.forEach((i) => {
                          if (i.value.raw) {
                            if (identifiers.length === 0) {
                              const classNames = i.value.raw.split(' ').filter(Boolean)
                              quasisClassNames.push(...classNames)
                              const value = utils.getClassNames(path, classNames, selectors, targetAttribute)
                              i.value.raw = ` ${value} `
                            }
                            if (identifiers.length === 1) {
                              const endWiths = i.end + 2 === identifiers[0].start
                              const startWiths = i.start - 1 === identifiers[0].end
                              if (endWiths) {
                                const classNames = i.value.raw.split(' ').filter(Boolean).slice(0, -1)
                                quasisClassNames.push(...classNames)
                                const value = utils.getClassNames(path, classNames, selectors, targetAttribute)
                                i.value.raw = ` ${value} ${i.value.raw.split(' ').slice(-1)}`
                              } else if (startWiths) {
                                const classNames = i.value.raw.split(' ').filter(Boolean)
                                quasisClassNames.push(...classNames)
                                const value = utils.getClassNames(path, classNames, selectors, targetAttribute)
                                i.value.raw = ` ${value}`
                              }
                            }
                          }
                        })
                      }
                      utils.setConsequentAndAlternateValue({ path, consequent, alternate, targetAttribute, quasisClassNames, selectors })
                      if (expressions) {
                        expressions.forEach((item) => {
                          const { consequent, alternate } = item
                          // ConditionalExpression（条件表达式）是一个编程和逻辑概念，它定义了一个测试条件，一个true表达式，以及一个可选的false表达式
                          if (item.type === 'ConditionalExpression') {
                            utils.setConsequentAndAlternateValue({ path, consequent, alternate, targetAttribute, quasisClassNames, selectors })
                          }
                          // LogicalExpression（逻辑表达式）是一种表达式，它用来表示逻辑上的“与”、“或”、“非”、“异或”等关系。
                          if (item.type === 'LogicalExpression') {
                            const { right } = item
                            if (right.value) {
                              const classNames = right.value.split(' ')
                              const value = utils.getClassNames(path, classNames, selectors, targetAttribute)
                              const newValue = value.split(' ')
                              const intersectionSelector = {}
                              if (quasisClassNames.length > 0) {
                                quasisClassNames.forEach((item) => {
                                  classNames.forEach((i) => {
                                    intersectionSelector[`${item}.${i}`] = true
                                    intersectionSelector[`${i}.${item}`] = true
                                    const value = utils.getClassNames(path, Object.keys(intersectionSelector), selectors, targetAttribute)
                                    newValue.push(...value.split(' '))
                                  })
                                })
                              }
                              right.value = newValue.filter((i) => !intersectionSelector[i]).join(' ')
                            }
                            if (right.type === 'LogicalExpression') {
                              if (right.right && right.right.type === 'StringLiteral') {
                                const classNames = right.right.value.split(' ')
                                const value = utils.getClassNames(path, classNames, selectors, targetAttribute)
                                const newValue = value.split(' ')
                                const intersectionSelector = {}
                                if (quasisClassNames.length > 0) {
                                  quasisClassNames.forEach((item) => {
                                    classNames.forEach((i) => {
                                      intersectionSelector[`${item}.${i}`] = true
                                      intersectionSelector[`${i}.${item}`] = true
                                      const value = utils.getClassNames(path, Object.keys(intersectionSelector), selectors, targetAttribute)
                                      newValue.push(...value.split(' '))
                                    })
                                  })
                                }
                                right.right.value = newValue.filter((i) => !intersectionSelector[i]).join(' ')
                              }
                            }
                          }
                        })
                      }
                      if (identifiers.length > 0) {
                        identifiers.forEach((i) => {
                          console.warn(`${JSON.stringify(i)} cant not to converted to tailwindcss  at ${root.source.input.file}. at ${filePath} \n`)
                        })
                      }
                    }
                  }
                },
              });
              if (opts.removeEmptyClassName !== false) {
                utils.clearEmptyClassName(ast, emptyRules, targetAttribute)
              }
              const newCode = generator(ast).code
              fs.writeFile(filePath, newCode);
            })
            .catch((error) => {
              throw root.error(`An error occurred while processing ${filePath}: \n ${error}`);
            });
        },
        Declaration(decl) {
          const { prop, parent, value } = decl;
          if (prop && parent.selector) {
            if (!utils.isInAtRule(decl)) {
              const ratio = opts.ratio || 1;
              const className = utils.getTailwindClassName(classNames, { prop, value, ratio, keyframesAtRules });
              if (className) {
                utils.getNestedSelectorsAndSelectorKey(classNames, parent.selector)
                  .forEach(({ key, nested, selector, pseudoName }) => {
                    if (!selectors[key]) {
                      selectors[key] = { selector, nested, className: {} }
                    }
                    if (!selectors[key].className) {
                      selectors[key].className = {}
                    }
                    if (!pseudoName) {
                      Object.assign(selectors[key].className, className)
                    }
                    if (pseudoName) {
                      if (!selectors[key].pseudo) {
                        selectors[key].pseudo = {
                          [pseudoName]: {}
                        }
                      }
                      parent.walkDecls((decl) => {
                        const { prop, value } = decl;
                        const className = utils.getTailwindClassName(classNames, { prop, value, ratio, keyframesAtRules });
                        if (className) {
                          if (!selectors[key].pseudo[pseudoName]) {
                            selectors[key].pseudo[pseudoName] = {}
                          }
                          Object.assign(selectors[key].pseudo[pseudoName], className)
                        } else {
                          unTransformDecls.add(`${prop}: ${value};`)
                        }
                      })
                    }
                  })
              } else {
                unTransformDecls.add(`${prop}: ${value};`)
              }
            }
          }
        },
        RuleExit(rule) {
          rule.walkDecls((decl) => {
            const { prop, value } = decl;
            const declaration = `${prop}: ${value};`;
            if (decl.parent.parent.name !== 'keyframes') {
              if (!unTransformDecls.has(declaration)) {
                decl.remove()
              }
            }
          })

          if (rule.nodes.length === 0) {
            rule.remove();
            if (opts.removeEmptyClassName !== false) {
              emptyRules[rule.selector] = selectors[rule.selector]
            }
          }
        },
        AtRule(atRule) {
          if (atRule.name === 'media') {
            atRule.walk((decl) => {
              const { prop, value, parent } = decl;
              if (prop && parent.selector) {
                const ratio = opts.ratio || 1;
                const mediaName = classNames['responsive-design'][atRule.params.replace(/\D/g, '') + 'px']
                const className = utils.getTailwindClassName(classNames, { prop, value, ratio, keyframesAtRules });
                if (className) {
                  utils.getNestedSelectorsAndSelectorKey(classNames, parent.selector)
                    .forEach(({ nested, key, selector, pseudoName }) => {
                      if (!selectors[key]) {
                        selectors[key] = { selector, nested, className: {} }
                      }
                      if (!pseudoName) {
                        if (!selectors[key].media) {
                          selectors[key].media = {
                            [mediaName]: {}
                          }
                        }
                        if (!selectors[key].media[mediaName]) {
                          selectors[key].media[mediaName] = {}
                        }
                        Object.assign(
                          selectors[key].media[mediaName],
                          Object.values(className).map((i) => `${mediaName}:${i}`)
                        )
                      }
                      if (pseudoName) {
                        if (!selectors[key].atRulePseudo) {
                          selectors[key].atRulePseudo = {
                            [pseudoName]: {}
                          }
                        }
                        decl.parent.walkDecls((decl) => {
                          const { prop, value } = decl;
                          const className = utils.getTailwindClassName(classNames, { prop, value, ratio, keyframesAtRules });
                          if (className) {
                            if (!selectors[key].atRulePseudo[pseudoName]) {
                              selectors[key].atRulePseudo[pseudoName] = {}
                            }
                            Object.assign(selectors[key].atRulePseudo[pseudoName], className)
                          } else {
                            unTransformDecls.add(`${prop}: ${value};`)
                          }
                        })
                      }
                    })
                } else {
                  unTransformDecls.add(`${prop}: ${value};`)
                }
              }
            })
          }
        },
        AtRuleExit(atRule) {
          if (atRule.nodes.length === 0) {
            atRule.remove()
          }
        },
        Comment(comment) {
          comment.remove()
        }
      }
    }
  };
};

module.exports.postcss = true;
